# Copyright (c) HySoP 2011-2024
#
# This file is part of HySoP software.
# See "https://particle_methods.gricad-pages.univ-grenoble-alpes.fr/hysop-doc/"
# for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Box-shaped domains definition.
"""
import warnings
import numpy as np
from hysop.constants import BoxBoundaryCondition, HYSOP_REAL, HYSOP_DEFAULT_TASK_ID
from hysop.domain.domain import Domain, DomainView
from hysop.tools.decorators import debug
from hysop.tools.numpywrappers import npw
from hysop.tools.htypes import check_instance, first_not_None, to_tuple
from hysop.tools.warning import HysopWarning
[docs]
class BoxView(DomainView):
__slots__ = ("_domain", "_topology_state")
[docs]
@debug
def __new__(cls, topology_state, domain=None, **kwds):
"""Create and initialize a BoxView."""
from hysop.topology.cartesian_topology import CartesianTopologyState
check_instance(topology_state, CartesianTopologyState)
check_instance(domain, Box, allow_none=True)
obj = super().__new__(cls, topology_state=topology_state, domain=domain, **kwds)
check_instance(obj._domain, Box)
return obj
def __init__(self, topology_state, domain=None, **kwds):
super().__init__(topology_state=topology_state, domain=domain, **kwds)
def __get_domain_attr(self, name):
"""Get a transposed domain attribute."""
return self._topology_state.transposed(getattr(self._domain, name))
def _get_length(self):
"""Box sides lengthes."""
return self.__get_domain_attr("_length")
def _get_origin(self):
"""Position of the lowest point of the box."""
return self.__get_domain_attr("_origin")
def _get_end(self):
"""Position of the greatest point of the box."""
return self.__get_domain_attr("_origin") + self.__get_domain_attr("_length")
def _get_lboundaries(self):
"""Left boundary conditions."""
return self.__get_domain_attr("_lboundaries")
def _get_rboundaries(self):
"""Right boundary conditions."""
return self.__get_domain_attr("_rboundaries")
def _get_boundaries(self):
"""Left and right boundary conditions as a tuple."""
return (
self.__get_domain_attr("_lboundaries"),
self.__get_domain_attr("_rboundaries"),
)
def _get_periodicity(self):
"""Numpy array mask, True is axis is periodic, else False."""
periodic = BoxBoundaryCondition.PERIODIC
is_lperiodic = self.__get_domain_attr("_lboundaries") == periodic
is_rperiodic = self.__get_domain_attr("_rboundaries") == periodic
if np.logical_xor(is_lperiodic, is_rperiodic).any():
msg = "Domain periodic boundaries have been mixed with another boundary "
msg += "on the same axe."
raise RuntimeError(msg)
return is_lperiodic
[docs]
def long_description(self):
"""
Return a long description of this Box as a string.
"""
s = f"{self.full_tag} | {self.dim}D rectangular box domain:"
s += f"\n *origin: {self.origin}"
s += f"\n *max_pos: {self.end}"
s += f"\n *length: {self.length}"
s += f"\n *left boundary conditions: {self.lboundaries.tolist()}"
s += f"\n *right boundary conditions: {self.rboundaries.tolist()}"
s += "\n"
return s
[docs]
def short_description(self):
"""
Return a short description of this Box as a string.
"""
return "{} (O=[{}], L=[{}], BC=[{}], current_task={})".format(
self.full_tag,
",".join(f"{val:1.1f}" for val in self.origin),
",".join(f"{val:1.1f}" for val in self.length),
self.format_boundaries(),
self.current_task(),
)
length = property(_get_length)
origin = property(_get_origin)
end = property(_get_end)
lboundaries = property(_get_lboundaries)
rboundaries = property(_get_rboundaries)
boundaries = property(_get_boundaries)
periodicity = property(_get_periodicity)
[docs]
class Box(BoxView, Domain):
"""
Box-shaped domain description.
"""
@debug
def __init__(
self,
length=None,
origin=None,
dim=None,
lboundaries=None,
rboundaries=None,
**kwds,
):
super().__init__(
length=length,
origin=origin,
dim=dim,
lboundaries=lboundaries,
rboundaries=rboundaries,
domain=None,
topology_state=None,
**kwds,
)
[docs]
@debug
def __new__(
cls,
length=None,
origin=None,
dim=None,
lboundaries=None,
rboundaries=None,
**kwds,
):
"""
Create or get an existing Box from a dimension, length and origin with specified
left and right boundary conditions.
Parameters
----------
length : array like of float, optional
Box sides lengthes. Default = [1.0, ...]
origin: array like of float, optional
Position of the lowest point of the box. Default [0.0, ...]
dim: int, optional
Dimension of the box.
lboundaries: array_like of BoxBoundaryCondition
Left boundary conditions.
rboundaries: array_like of BoxBoundaryCondition
Right boundary conditions.
Attributes
----------
dim: int
Dimension of the box.
length : np.ndarray of HYSOP_REAL
Box sides lengthes.
origin: np.ndarray of HYSOP_REAL
Position of the lowest point of the box.
end: np.ndarray of HYSOP_REAL
Position of the greatest point of the box.
lboundaries: np.ndarray of BoxBoundaryCondition
Left boundary conditions.
rboundaries: np.ndarray of BoxBoundaryCondition
Right boundary conditions.
boundaries: tuple of np.ndarray of BoxBoundaryCondition
Left and right boundary conditions as a tuple.
periodicity: np.ndarray of bool
Numpy array mask, True is axis is periodic, else False.
"""
from hysop.topology.cartesian_topology import CartesianTopologyState
check_instance(dim, int, minval=1, allow_none=True)
check_instance(
length,
(np.ndarray, list, tuple),
values=(np.integer, int, float),
allow_none=True,
)
check_instance(
origin,
(np.ndarray, list, tuple),
values=(np.integer, int, float),
allow_none=True,
)
check_instance(
lboundaries,
(np.ndarray, list, tuple),
values=BoxBoundaryCondition,
allow_none=True,
)
check_instance(
rboundaries,
(np.ndarray, list, tuple),
values=BoxBoundaryCondition,
allow_none=True,
)
if (length is None) and (origin is None) and (dim is None):
msg = "At least one of the following parameters should be given: length, origin, dim."
raise ValueError(msg)
dim = first_not_None(dim, 0)
length = to_tuple(first_not_None(length, 1.0))
origin = to_tuple(first_not_None(origin, 0.0))
dim = max(dim, len(length), len(origin))
if len(length) == 1:
length *= dim
if len(origin) == 1:
origin *= dim
length = npw.asrealarray(length)
origin = npw.asrealarray(origin)
check_instance(length, np.ndarray, size=dim)
check_instance(origin, np.ndarray, size=dim)
assert (length >= 0.0).all(), "length < 0"
lboundaries = npw.asarray(
first_not_None(lboundaries, (BoxBoundaryCondition.PERIODIC,) * dim)
)
rboundaries = npw.asarray(
first_not_None(rboundaries, (BoxBoundaryCondition.PERIODIC,) * dim)
)
assert lboundaries.size == rboundaries.size == dim
for i, (lb, rb) in enumerate(zip(lboundaries, rboundaries)):
if (lb == BoxBoundaryCondition.PERIODIC) ^ (
rb == BoxBoundaryCondition.PERIODIC
):
msg = (
f"FATAL ERROR: Periodic BoxBoundaryCondition mismatch on axis {i}."
)
msg += "\nGot:"
msg += f"\n *lboundaries: {lboundaries}"
msg += f"\n *rboundaries: {rboundaries}"
raise ValueError(msg)
nper = npw.sum(lboundaries == BoxBoundaryCondition.PERIODIC)
if (nper > 0) and not all(lboundaries[:nper] == BoxBoundaryCondition.PERIODIC):
msg = "\nPeriodic boundary conditions should be on last axes (ie. Z,Y,X,...), got "
msg += f"periodicity {lboundaries==BoxBoundaryCondition.PERIODIC}."
msg += "\nAll spectral solvers (including Poisson solvers) will fail with an error."
msg += "\nPlease permute axes prior to problem description."
msg += "\nSpecified boundaries were:"
msg += f"\n *lboundaries: {lboundaries}"
msg += f"\n *rboundaries: {rboundaries}"
warnings.warn(msg, HysopWarning)
# double check types, to be sure RegisteredObject will work as expected
check_instance(dim, int)
check_instance(length, np.ndarray, dtype=HYSOP_REAL)
check_instance(origin, np.ndarray, dtype=HYSOP_REAL)
check_instance(lboundaries, np.ndarray, dtype=object)
check_instance(rboundaries, np.ndarray, dtype=object)
npw.set_readonly(length, origin, lboundaries, rboundaries)
topology_state = CartesianTopologyState(dim, HYSOP_DEFAULT_TASK_ID)
obj = super().__new__(
cls,
length=length,
origin=origin,
dim=dim,
lboundaries=lboundaries,
rboundaries=rboundaries,
domain=None,
topology_state=topology_state,
**kwds,
)
if not obj.obj_initialized:
obj._length = length
obj._origin = origin
obj._lboundaries = lboundaries
obj._rboundaries = rboundaries
return obj
[docs]
def view(self, topology_state):
"""Return a view of this domain altered by some topology_state."""
return BoxView(domain=self, topology_state=topology_state)